Commands and Queries
Note: Spartacus 4.x is no longer maintained. Please upgrade to the latest version.
Note: Spartacus 4.x was tested with SAP Commerce Cloud versions 1905 to 2205. Spartacus 4.x has not been verified to work with (and is not guaranteed to work with) SAP Commerce Cloud 2211 or later releases.
Commands and queries provide a robust and simplified way to handle state (in other words, loading and caching), and to execute actions against the back end system. This is similar to the way that libraries such as React Query and SWR approach the problem of handling the state coming from the API in a single page application. A large part of the front end application state comes from the back end. Redux architecture, which is often used for state management, was not created with this type of state in mind, and requires a lot of boilerplate to support it. Different types of state require different solutions, and commands and queries are designed to handle the state coming from the API in Spartacus. In a lot of cases, commands and queries will replace NgRx for default Spartacus libraries, and will make most implementations simpler, with better, more consistent error handling, while also leveraging the Spartacus events framework.
Table of Contents
- Commands Overview
- Command Definition
- Exposing Commands in Facade Services
- Subscribing to Commands
- Queries Overview
- Query Definition
- Exposing Queries in Facade Services
Commands Overview
Commands represent an action that can change the state of the system, usually by issuing a REST call to the back end. Commands can return a result, and can be executed while taking an execution strategy into consideration. Each command execution returns an observable, which emits (with an optional success result) and then completes when the command finishes, or throws an error when the command execution results in an error.
Subscribing to the result observable does not determine command execution, so it is optional.
Command Definition
A command can be defined as a property of a class by storing the result of the CommandService.create
factory method call.
Commands have the following parameters:
- a function that dispatches the command (usually a call to the connector)
- an options object (usually to specify a strategy)
The following is an example:
protected updateCommand: Command<{ details: User }> = this.command.create(
(payload) =>
this.userIdService.takeUserId(true).pipe(
switchMap((uid) =>
this.userProfileConnector.update(uid, payload.details)
),
{
strategy: CommandStrategy.Queue,
}
);
The available strategies are the following:
Parallel
executes all commands in parallel.Queue
queues commands and executes them in order (this is the default strategy).CancelPrevious
starts a new execution of the command and cancels the previous one if it has not finished (the result stream for the previous execution will complete without emission).ErrorPrevious
starts a new execution of the command and throws an error for the previous one if it has not finished (the result stream for the previous execution will throw an error).
Exposing Commands in Facade Services
Commands are meant to be exposed as methods that calls can execute on the command class, and that return a result observable. As mentioned earlier, the call can simply invoke a method to execute the command. The following is an example:
update(details: User): Observable<unknown> {
return this.updateCommand.execute({ details });
}
Subscribing to Commands
Subscribing to the result observables does not determine command execution, so it is optional.
However, there are still good reasons for subscribing to commands, such as the following:
-
To compose command streams with other streams (for example, by using
switchMap
to switch to another stream), you need to subscribe to the command, otherwise it will not work. In the following example, theswitchMap
runs because, in the final line, there is a subscription to theupdate
command:this.userFacade .update({}) .pipe( switchMap(() => { // this would be ignored without a subscription return someStream$; }) ) .subscribe();
-
Depending on your unit test setup, you could be mocking the command with a fake stream. This requires you to make a subscription for it to run.
Queries Overview
Queries expose some state of the system, usually by fetching it from the back end, caching it for future use, and keeping it fresh by reloading it when needed. Each query exposes loading flags as part of the data stream.
Loading of the data is automatically triggered by the first subscriber to the query.
Query Definition
A query can be defined as a property of a class by storing the result of the QueryService.create
factory method call.
Queries have the following parameters:
- a function that returns the value of the query (usually a call to the connector)
- an options object (usually to specify triggers for reloading and resetting)
The following is an example:
protected titleQuery: Query<Title[]> = this.query.create(
() => this.userProfileConnector.getTitles(),
{
reloadOn: [LanguageSetEvent],
}
);
The reloadOn
and resetOn
triggers accept events or Observable
streams. Each emission of this kind of observable triggers a data reload (for reloadOn
triggers) or a data reset (for resetOn
triggers). The main difference between reload
and reset
is that reset
clears the query state immediately, while reload
just updates it when new data is loaded.
A good use case for the reload
trigger is with language and currency change events. In this scenario, you would most likely want to update all language or currency dependent content as soon as these events occur, but without introducing layout flickering, and without showing loaders. With the reload
trigger, queries are reloaded in the background after the language or currency change, while still showing current values. When you get a new response from the API in the correct language, the UI is updated with the new values, thereby providing a smooth user experience.
On the other hand, reset
triggers are a great way to react to events that potentially introduce significant changes in the back end state. One example would be with the OrderPlacedEvent
, which is an event that indicates that the current cart was ordered, and that now it is empty. In this case, as soon as the OrderPlacedEvent
event happens, the current cart state no longer seems valid, so it is safer to reset it to the value undefined
, and load the cart data again with an updated API state (which would most likely be an empty cart). Additionally, Spartacus can react to a state reset by showing a loading indicator on the cart component, while the app waits for the API response.
Exposing Queries in Facade Services
Queries can either expose a single value as an observable, or they can expose the entire state, including loading and error flags.
The following is an example of a query that exposes a single value as an observable:
getTitles(): Observable<Title[]> {
return this.titleQuery.get();
}
The following is an example of a query that exposes the entire state, with loading and error flags included:
getTitlesState(): Observable<QueryState<Title[]>> {
return this.titleQuery.getState();
}